﻿/*
 * Copyright (c) 2012 Lambert van Lieshout, YUMMO Software Development
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
*/

namespace Yummo.BlackBox
{
    using System;
    using System.Collections.Generic;
    using System.Collections;
    using System.Diagnostics;
    using System.Reflection;

    /// <summary>
    /// Event write delegate.
    /// </summary>
    /// <param name="message"></param>
    public delegate void EventWrite(EventMessage message);

    /// <summary>
    /// Black Box Manager. Handles log writing on same thread.
    /// </summary>
    public class BlackBoxManager
    {
        /// <summary>
        /// Collection of event writers. These will all be used when for event writing.
        /// </summary>
        protected List<EventWriterHolder>   _writers;
        
        /// <summary>
        /// Fallback event writer. This will be used when a primary event writer throws an exception.
        /// </summary>
        protected EventWriterHolder         _writeFallBack;

        /// <summary>
        /// Constructor of the event logger.
        /// </summary>
        public BlackBoxManager()
        {
            _writers = new List<EventWriterHolder>();
        }

        #region Register methods

        /// <summary>
        /// Register a event writer which will be triggered at given level.
        /// </summary>
        /// <param name="level">Event level to trigger the writer.</param>
        /// <param name="write">Event writer delegate.</param>
        public void RegisterWriter(EventTypes level, EventWrite write)
        {
            if (write == null) throw new ArgumentNullException("writer", "Event writer cannot be NULL");
            _writers.Add(new EventWriterHolder(level, write));
        }

        /// <summary>
        /// Register a fallback event writer which will be triggered when other event writers throw an exception on the Write method.
        /// </summary>
        /// <param name="write">Event writer delegate.</param>
        public void RegisterFallbackWriter(EventWrite write)
        {
            if (write == null) throw new ArgumentNullException("writer", "Event writer cannot be NULL");
            _writeFallBack = new EventWriterHolder(BlackBoxUtilities.GetAllEventTypes(), write);
        }

        #endregion

        #region Write methods

        /// <summary>
        /// Write event message. Use fallback event writer when primairy writer throws an exception. This write method blocks until the writer is finished.
        /// </summary>
        /// <param name="message">Event message to write</param>
        public virtual void Write(EventMessage message)
        {
            if (message == null) return;
            for (int i = 0; i < _writers.Count; i++)
            {
                if (((short)_writers[i].Level & (short)message.EventType) != (short)message.EventType) continue;
                try
                {
                    _writers[i].Write(message);
                }
                catch (Exception exception)
                {
                    if (_writeFallBack != null && _writeFallBack.Write != null)
                    {
                        EventMessage errorEvent = ExceptionToEventMessage(exception);
                        _writeFallBack.Write(message);
                        _writeFallBack.Write(errorEvent);
                    }
                }
            }
        }

        /// <summary>
        /// Write exception to event log
        /// </summary>
        /// <param name="exception">Exception to log</param>
        /// <param name="host">Host</param>
        public void Write(Exception exception, string host = null)
        {
            Write(ExceptionToEventMessage(exception, host));
        }

        /// <summary>
        /// Write custom event
        /// </summary>
        /// <param name="type">Event level</param>
        /// <param name="data">Data</param>
        /// <param name="parameters">Parameters</param>
        /// <param name="service">Service</param>
        /// <param name="host">Host</param>
        public void Write(EventTypes type, string data, IDictionary parameters = null, string service = null, string host = null)
        {
            EventMessage message = new EventMessage();
            message.Data = data;
            message.EventType = type;
            message.Host = (host == null) ? System.Net.Dns.GetHostName() : host;
            message.Thread = String.Concat(System.Threading.Thread.CurrentThread.Name, " (", System.Threading.Thread.CurrentThread.ManagedThreadId, ")");
            message.Parameters = "";
            if (parameters != null && parameters.Count > 0)
            {
                foreach (DictionaryEntry item in parameters)
                {
                    string value = (item.Value.GetType().IsPrimitive) ? item.Value.ToString() : String.Concat("\"", item.Value.ToString(), "\"");
                    message.Parameters += String.Concat("@", item.Key, "=", value, ",");
                }
            }
            if (service == null)
            {
                System.Reflection.MethodBase m = new System.Diagnostics.StackTrace().GetFrame(1).GetMethod();
                message.Service = String.Concat(m.ReflectedType.FullName, ".", m.Name);
            }
            else
            {
                message.Service = service;
            }
            Write(message);
        }

        #endregion

        #region Exception conversion

        /// <summary>
        /// Converts exception into a event message.
        /// </summary>
        /// <param name="exception">Exception to convert.</param>
        /// <param name="host">Host</param>
        /// <returns>Event message</returns>
        protected EventMessage ExceptionToEventMessage(Exception exception, string host = null)
        {
            if (exception == null) throw new ArgumentNullException("exception", "Exception cannot be NULL.");
            EventMessage message = new EventMessage();
            if (exception.Source == null)
            {
                if (exception.TargetSite == null)
                {
                    MethodBase m = new StackTrace().GetFrame(2).GetMethod();
                    message.Service = String.Concat(m.ReflectedType.FullName, ".", m.Name);
                }
                else
                {
                    message.Service = String.Concat(exception.TargetSite.ReflectedType.FullName, ".", exception.TargetSite.Name);
                }
            }
            else
            {
                message.Service = exception.Source;
            }            
            message.EventType = EventTypes.Error;
            message.Thread = String.Concat(System.Threading.Thread.CurrentThread.Name, " (", System.Threading.Thread.CurrentThread.ManagedThreadId, ")");
            message.Parameters = "";
            foreach (DictionaryEntry item in exception.Data)
            {
                string value = (item.Value.GetType().IsPrimitive) ? item.Value.ToString() : String.Concat("\"", item.Value.ToString(), "\"");
                message.Parameters = String.Concat(message.Parameters, "@", item.Key.ToString(), "=", value, ",");
            }
            message.Data = ExceptionToString(exception);
            message.Host = (host == null) ? System.Net.Dns.GetHostName() : host;
            return message;
        }

        /// <summary>
        /// Converts exception to a formatted string.
        /// </summary>
        /// <param name="exception">Exception to convert.</param>
        /// <returns>String with exception.</returns>
        protected string ExceptionToString(Exception exception)
        {
            string result = "";
            result  += "Message: "          + exception.Message;
            result  += "\r\nHelplink: "     + exception.HelpLink;
            result  += "\r\nSource: "       + exception.Source;
            result  += "\r\nStackTrace: "   + exception.StackTrace;
            if (exception.InnerException != null)
            {
                result = String.Concat(result, "\r\n\r\nInner exception:\r\n", ExceptionToString(exception.InnerException));
            }
            return result;
        }

        #endregion

        /// <summary>
        /// Holder class for event writers and level when to trigger the writer.
        /// </summary>
        protected class EventWriterHolder
        {
            /// <summary>
            /// Constructor of EventWriteHolder.
            /// </summary>
            /// <param name="level">Level when to trigger the event writer.</param>
            /// <param name="write">Event writer method.</param>
            public EventWriterHolder(EventTypes level, EventWrite write)
            {
                Level = level;
                Write = write;
            }

            /// <summary>
            /// Gets level when to trigger the event writer.
            /// </summary>
            public EventTypes   Level   { get; private set; }
            
            /// <summary>
            /// Gets event writer method.
            /// </summary>
            public EventWrite   Write   { get; private set; }
        }
    }
}
